﻿#region Usings

using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Text;
using System.Xml;
using System.Xml.Serialization;

using Gonte.Reflection; 

#endregion

namespace Gonte.Xml.Serialization
{
    /// <summary>
    /// Serializes a single object
    /// </summary>
    public class XmlObjectSerializer
    {
        #region Constructors

        protected XmlObjectSerializer(Object @object)
        {
            object_ = @object;
        }

        #endregion

        #region Methods

        public static XmlObjectSerializer GetSerializer(object @object)
        {
            ExceptionThrower.ThrowWhenNull("Object", @object);

            if (@object is IXmlSerializableObject)
            {
                IXmlSerializableObject serializableObject = @object as IXmlSerializableObject;
                return serializableObject.GetSerializer();
            }

            if (PrimitiveType.IsPrimitive(@object)
                || @object.GetType().IsEnum)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlPrimitiveSerializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlPrimitiveSerializer(@object);
            }

            if (@object is IDictionary)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlDictionarySerializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlDictionarySerializer(@object);
            }

            if (@object is ICollection)
            {
                Debug.WriteLine(
                    string.Format(
                        "Creating a XmlCollectionSerializer for object of type: {0}",
                        @object.GetType().FullName));

                return new XmlCollectionSerializer(@object);
            }

            Debug.WriteLine(
                    string.Format(
                        "Creating a XmlObjectSerializer for object of type: {0}",
                        @object.GetType().FullName));

            return new XmlObjectSerializer(@object);
        }
        
        public string SerializeToString()
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = Encoding.UTF8;

            StringBuilder stringBuilder = new StringBuilder();
            using (XmlWriter writer = XmlWriter.Create(stringBuilder, settings))
            {
                writer.WriteStartDocument();
                Serialize(writer, null);
                writer.WriteEndDocument();
            }

            return stringBuilder.ToString();
        }

        public void Serialize(string fileName)
        {
            XmlWriterSettings settings = new XmlWriterSettings();
            settings.Indent = true;
            settings.Encoding = Encoding.UTF8;

            using (XmlWriter writer = XmlWriter.Create(fileName, settings))
            {
                writer.WriteStartDocument();
                Serialize(writer, null);
                writer.WriteEndDocument();
            }
        }

        #endregion

        #region Overridables

        /// <summary>
        /// Serializes an object using an existing open xml writer
        /// </summary>
        /// <param name="writer"></param>
        public virtual void Serialize(XmlWriter writer, XmlSerializationContext context)
        {
            string elementName = GetElementName(context);
            writer.WriteStartElement(elementName);

            Type type = object_.GetType();
            TypeXmlMapping typeXmlMapping = TypeXmlMapping.GetMapping(type);

            // In order to set the attributes of the parent object we need to know about the attributes of its children
            // That's why we fill the attributes of the parent after populating the children
            if (null == context)
            {
                if (typeXmlMapping.HasAbstractMembers)
                {
                    writer.WriteAttributeString("xmlns", "xsi", null, "http://www.w3.org/2001/XMLSchema-instance");
                }
            }
            else
            {
                if (context.SerializeType)
                {
                    writer.WriteAttributeString("xsi", "type", null, type.FullName);
                }
            }

            TypeAccessor typeAccessor = TypeAccessor.GetTypeAccessor(type);
            List<PropertyContext> complexTypes = new List<PropertyContext>();
            
            foreach (TypeXmlMapping.PropertyXmlMapping propertyXmlMapping in typeXmlMapping.PropertyMappings)
            {
                if (propertyXmlMapping.Save)
                {
                    Object value = typeAccessor.GetValue(object_, propertyXmlMapping.PropertyName);

                    if (value != null)
                    {
                        if (PrimitiveType.IsPrimitive(value))
                        {
                            XmlObjectSerializer serializer = XmlObjectSerializer.GetSerializer(value);

                            XmlSerializationContext childContext = new XmlSerializationContext();
                            childContext.Parent = object_;
                            childContext.PropertyName = propertyXmlMapping.PropertyName;
                            childContext.Attributes = type.GetProperty(propertyXmlMapping.PropertyName).GetCustomAttributes(false);
                            childContext.Level = (context == null) ? 1 : ++context.Level;

                            serializer.Serialize(writer, childContext);
                        }
                        else // Postpone the serialization until we finished with the simple objects (attributes)
                        {
                            complexTypes.Add(new PropertyContext(value, propertyXmlMapping));
                        }
                    }
                }
            }

            foreach (PropertyContext propertyContext in complexTypes) // Add the complex types
            {
                XmlObjectSerializer serializer = XmlObjectSerializer.GetSerializer(propertyContext.Object);

                TypeXmlMapping.PropertyXmlMapping mapping = propertyContext.PropertyXmlMapping;

                XmlSerializationContext childContext = new XmlSerializationContext();
                childContext.Parent = object_;
                childContext.PropertyName = mapping.PropertyName;
                childContext.Attributes = type.GetProperty(mapping.PropertyName).GetCustomAttributes(false);
                childContext.SerializeType = mapping.IsAbstract;
                childContext.Level = (context == null) ? 1 : ++context.Level;

                serializer.Serialize(writer, childContext);
            }

            writer.WriteEndElement();
        }

        protected virtual string GetElementName(XmlSerializationContext context)
        {
            Type type = object_.GetType();
            string elementName = type.Name;

            if (context != null
                && context.Attributes != null)
            {
                XmlElementAttribute [] xmlElementAttributes = CustomAttribute.FindAttributes<XmlElementAttribute>(context.Attributes);

                if (null != xmlElementAttributes
                    && xmlElementAttributes.Length > 0)
                {
                    foreach(XmlElementAttribute xmlElementAttribute in xmlElementAttributes)
                    {
                        if (!string.IsNullOrEmpty(xmlElementAttribute.ElementName))
                        {
                            elementName = xmlElementAttribute.ElementName;
                            break;
                        }
                    }
                }
                else if (!(context.Parent is ICollection))
                {
                    elementName = context.PropertyName;
                }
                
                Debug.WriteLine(string.Format("Name of element is: {0} for type: {1} and property name: {2}",
                    elementName,
                    type.Name,
                    context.PropertyName));
            }
            else // It is the root
            {
                XmlRootAttribute xmlRootAttribute = CustomAttribute.GetCustomAttribute<XmlRootAttribute>(type, false);
                if (xmlRootAttribute != null
                    && !string.IsNullOrEmpty(xmlRootAttribute.ElementName))
                {
                    elementName = xmlRootAttribute.ElementName;
                }

                Debug.WriteLine(string.Format("Name of element is: {0} for root type: {1}",
                    elementName,
                    type.Name));
            }
            
            return elementName;
        }

        #endregion

        #region Internal Types

        internal struct PropertyContext
        {
            internal PropertyContext(object @object, TypeXmlMapping.PropertyXmlMapping propertyXmlMapping)
            {
                Object = @object;
                PropertyXmlMapping = propertyXmlMapping;
            }

            internal TypeXmlMapping.PropertyXmlMapping PropertyXmlMapping;
            internal object Object;
        }

        #endregion

        #region Fields

        protected readonly object object_;

        #endregion
    }
}
